<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
    <title>贪吃蛇 Snake</title>
    <style>
      :root {
        --bg: #0e1013;
        --panel: #161a1f;
        --panel-2: #1c222a;
        --text: #e6e8eb;
        --muted: #9aa3ad;
        --accent: #3ddc97;
        --accent-2: #4cc9f0;
        --danger: #ef476f;
        --grid: #222831;
      }

      * { box-sizing: border-box; }
      html, body { height: 100%; }
      body {
        margin: 0;
        background: radial-gradient(1200px 800px at 20% 10%, #10151b 0%, var(--bg) 60%);
        color: var(--text);
        font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 16px;
      }

      .container {
        width: 100%;
        max-width: 720px;
        display: grid;
        grid-template-rows: auto auto 1fr auto;
        gap: 12px;
      }

      header {
        display: flex;
        align-items: center;
        justify-content: space-between;
      }

      h1 {
        font-size: 20px;
        margin: 0;
        letter-spacing: 0.5px;
      }

      .scoreboard {
        display: flex;
        gap: 12px;
        align-items: baseline;
      }

      .badge {
        background: linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%);
        border: 1px solid #242b34;
        border-radius: 10px;
        padding: 8px 12px;
        font-weight: 600;
      }

      .board-wrap {
        background: linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%);
        border: 1px solid #242b34;
        border-radius: 16px;
        padding: 12px;
        box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
      }

      canvas {
        width: 100%;
        aspect-ratio: 1 / 1;
        image-rendering: pixelated;
        border-radius: 12px;
        display: block;
        background: #0b0e12;
      }

      .controls {
        display: grid;
        grid-template-columns: 1fr;
        gap: 10px;
        background: linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%);
        border: 1px solid #242b34;
        border-radius: 12px;
        padding: 12px;
      }

      .row {
        display: flex;
        gap: 8px;
        align-items: center;
        flex-wrap: wrap;
      }

      button, input[type="range"] {
        -webkit-tap-highlight-color: transparent;
      }

      .btn {
        background: #11161c;
        border: 1px solid #25303b;
        color: var(--text);
        border-radius: 10px;
        padding: 10px 14px;
        font-weight: 600;
        cursor: pointer;
        transition: transform 0.04s ease, background 0.2s ease, border-color 0.2s ease;
      }
      .btn:active { transform: translateY(1px) scale(0.99); }
      .btn.primary { background: linear-gradient(180deg, #123524, #0d2b1d); border-color: #204a36; color: #b6ffe0; }
      .btn.danger { background: linear-gradient(180deg, #2b1016, #220b10); border-color: #40202a; color: #ffd6e0; }

      .slider {
        appearance: none;
        width: 220px;
        height: 6px;
        border-radius: 999px;
        background: #1f2630;
        outline: none;
      }
      .slider::-webkit-slider-thumb {
        appearance: none;
        width: 20px;
        height: 20px;
        border-radius: 50%;
        background: radial-gradient(circle at 30% 30%, var(--accent), var(--accent-2));
        box-shadow: 0 2px 8px rgba(60, 220, 151, 0.4);
        border: 2px solid #0c1117;
      }

      .dpad {
        margin-top: 6px;
        display: grid;
        grid-template-columns: 64px 64px 64px;
        grid-template-rows: 64px 64px 64px;
        gap: 8px;
        justify-content: center;
        user-select: none;
      }
      .dpad .spacer { visibility: hidden; }
      .dkey {
        background: #11161c;
        border: 1px solid #25303b;
        color: var(--muted);
        border-radius: 12px;
        display: flex;
        align-items: center;
        justify-content: center;
        font-weight: 700;
        font-size: 18px;
        cursor: pointer;
        transition: transform 0.04s ease, background 0.2s ease, border-color 0.2s ease;
      }
      .dkey:active { transform: translateY(1px) scale(0.98); }

      .hint { color: var(--muted); font-size: 12px; }
      .grow { flex: 1; }
    </style>
  </head>
  <body>
    <div class="container">
      <header>
        <h1>贪吃蛇 Snake</h1>
        <div class="scoreboard">
          <div class="badge" id="scoreBox">得分：<span id="score">0</span></div>
          <div class="badge" id="bestBox">最高：<span id="best">0</span></div>
        </div>
      </header>

      <div class="board-wrap">
        <canvas id="board" width="480" height="480"></canvas>
      </div>

      <div class="controls">
        <div class="row">
          <button class="btn primary" id="btnToggle">开始</button>
          <button class="btn" id="btnRestart">重开</button>
          <span class="grow"></span>
          <label>速度</label>
          <input class="slider" id="speed" type="range" min="5" max="20" step="1" value="10" />
          <span id="speedLabel">中速</span>
        </div>
        <div class="row hint">键盘方向键 / WASD 控制方向，空格暂停/继续。手机可点按方向或滑动操作。</div>
        <div class="dpad" id="dpad">
          <div class="spacer"></div>
          <div class="dkey" data-dir="Up">▲</div>
          <div class="spacer"></div>
          <div class="dkey" data-dir="Left">◀</div>
          <div class="spacer"></div>
          <div class="dkey" data-dir="Right">▶</div>
          <div class="spacer"></div>
          <div class="dkey" data-dir="Down">▼</div>
          <div class="spacer"></div>
        </div>
      </div>
    </div>

    <script>
      const canvas = document.getElementById('board');
      const ctx = canvas.getContext('2d');
      const scoreEl = document.getElementById('score');
      const bestEl = document.getElementById('best');
      const btnToggle = document.getElementById('btnToggle');
      const btnRestart = document.getElementById('btnRestart');
      const speedSlider = document.getElementById('speed');
      const speedLabel = document.getElementById('speedLabel');
      const dpad = document.getElementById('dpad');

      // Game configuration
      const gridCells = 20; // 20x20 grid
      const cellSize = canvas.width / gridCells; // 480 / 20 = 24px
      const wallColor = '#0f141a';
      const gridColor = '#121821';
      const snakeHeadColor = '#3ddc97';
      const snakeBodyColor = '#2fb37b';
      const foodColor = '#ffbd3c';

      /** @typedef {{x:number, y:number}} Point */

      /** State */
      let snake = []; // Array<Point>
      let direction = { x: 1, y: 0 }; // current move vector
      let nextDirection = { x: 1, y: 0 }; // buffered vector
      let food = { x: 10, y: 10 };
      let score = 0;
      let best = Number(localStorage.getItem('snake_best') || '0');
      bestEl.textContent = String(best);
      let isPaused = true;
      let isGameOver = false;
      let loopId = null; // number | null
      let baseTickMs = sliderToMs(parseInt(speedSlider.value, 10));

      // Helpers
      function sliderToMs(v) {
        // Slider: 5(slowest) .. 20(fastest) -> 260ms .. 70ms approx
        const t = (v - 5) / (20 - 5); // 0..1
        const ms = Math.round(260 - t * 190);
        return ms;
      }
      function updateSpeedLabel() {
        const v = parseInt(speedSlider.value, 10);
        if (v <= 7) speedLabel.textContent = '慢速';
        else if (v <= 13) speedLabel.textContent = '中速';
        else speedLabel.textContent = '快速';
      }
      updateSpeedLabel();

      function resetGame() {
        const startX = Math.floor(gridCells / 3);
        const startY = Math.floor(gridCells / 2);
        snake = [
          { x: startX, y: startY },
          { x: startX - 1, y: startY },
          { x: startX - 2, y: startY }
        ];
        direction = { x: 1, y: 0 };
        nextDirection = { x: 1, y: 0 };
        score = 0;
        scoreEl.textContent = '0';
        isGameOver = false;
        placeFood();
        draw();
      }

      function startLoop() {
        stopLoop();
        isPaused = false;
        btnToggle.textContent = '暂停';
        const tickMs = baseTickMs;
        loopId = setInterval(gameTick, tickMs);
      }
      function stopLoop() {
        if (loopId !== null) {
          clearInterval(loopId);
          loopId = null;
        }
      }

      function togglePause() {
        if (isGameOver) return;
        if (isPaused) startLoop(); else { isPaused = true; btnToggle.textContent = '开始'; stopLoop(); }
      }

      function gameTick() {
        // apply nextDirection once per tick
        if ((nextDirection.x !== -direction.x || nextDirection.y !== -direction.y)) {
          direction = nextDirection;
        }

        const head = snake[0];
        const newHead = { x: head.x + direction.x, y: head.y + direction.y };

        // wall collision
        if (newHead.x < 0 || newHead.x >= gridCells || newHead.y < 0 || newHead.y >= gridCells) {
          return onGameOver();
        }
        // self collision
        for (let i = 0; i < snake.length; i++) {
          if (snake[i].x === newHead.x && snake[i].y === newHead.y) {
            return onGameOver();
          }
        }

        // move
        snake.unshift(newHead);
        const ateFood = newHead.x === food.x && newHead.y === food.y;
        if (ateFood) {
          score += 1;
          scoreEl.textContent = String(score);
          if (score > best) {
            best = score;
            localStorage.setItem('snake_best', String(best));
            bestEl.textContent = String(best);
          }
          placeFood();
          // Optional: slight speed up for each food
          // baseTickMs = Math.max(60, Math.round(baseTickMs * 0.98));
          // restart interval at new speed
          stopLoop();
          loopId = setInterval(gameTick, baseTickMs);
        } else {
          snake.pop();
        }

        draw();
      }

      function onGameOver() {
        stopLoop();
        isGameOver = true;
        isPaused = true;
        btnToggle.textContent = '开始';
        drawGameOver();
      }

      function placeFood() {
        while (true) {
          const x = Math.floor(Math.random() * gridCells);
          const y = Math.floor(Math.random() * gridCells);
          const onSnake = snake.some(p => p.x === x && p.y === y);
          if (!onSnake) { food = { x, y }; return; }
        }
      }

      function draw() {
        // background
        ctx.fillStyle = wallColor;
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        // grid
        ctx.strokeStyle = gridColor;
        ctx.lineWidth = 1;
        for (let i = 0; i <= gridCells; i++) {
          // vertical
          ctx.beginPath();
          ctx.moveTo(i * cellSize + 0.5, 0);
          ctx.lineTo(i * cellSize + 0.5, canvas.height);
          ctx.stroke();
          // horizontal
          ctx.beginPath();
          ctx.moveTo(0, i * cellSize + 0.5);
          ctx.lineTo(canvas.width, i * cellSize + 0.5);
          ctx.stroke();
        }

        // food
        drawRoundedCell(food.x, food.y, '#0e0b01', foodColor);

        // snake
        for (let i = 0; i < snake.length; i++) {
          const p = snake[i];
          const colorBg = i === 0 ? '#062116' : '#081810';
          const colorFg = i === 0 ? snakeHeadColor : snakeBodyColor;
          drawRoundedCell(p.x, p.y, colorBg, colorFg);
        }

        if (isGameOver) {
          drawGameOver();
        }
      }

      function drawRoundedCell(gx, gy, bg, fg) {
        const x = gx * cellSize;
        const y = gy * cellSize;
        const pad = Math.max(2, Math.floor(cellSize * 0.12));
        const r = Math.floor(cellSize * 0.2);

        // shadow/bg
        ctx.fillStyle = bg;
        roundRect(ctx, x + pad, y + pad, cellSize - pad * 2, cellSize - pad * 2, r);
        ctx.fill();

        // foreground
        const inset = Math.max(1, Math.floor(cellSize * 0.06));
        ctx.fillStyle = fg;
        roundRect(
          ctx,
          x + pad + inset,
          y + pad + inset,
          cellSize - (pad + inset) * 2,
          cellSize - (pad + inset) * 2,
          r
        );
        ctx.fill();
      }

      function roundRect(ctx, x, y, w, h, r) {
        ctx.beginPath();
        ctx.moveTo(x + r, y);
        ctx.arcTo(x + w, y, x + w, y + h, r);
        ctx.arcTo(x + w, y + h, x, y + h, r);
        ctx.arcTo(x, y + h, x, y, r);
        ctx.arcTo(x, y, x + w, y, r);
        ctx.closePath();
      }

      function drawGameOver() {
        draw(); // draw current frame first if called directly
        ctx.fillStyle = 'rgba(0,0,0,0.5)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = '#ffffff';
        ctx.font = 'bold 28px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial';
        ctx.textAlign = 'center';
        ctx.fillText('游戏结束', canvas.width / 2, canvas.height / 2 - 10);
        ctx.font = '16px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial';
        ctx.fillText('按 重开 开始新一局', canvas.width / 2, canvas.height / 2 + 20);
      }

      // Input handling
      const keyToDir = {
        ArrowUp: { x: 0, y: -1 },
        ArrowDown: { x: 0, y: 1 },
        ArrowLeft: { x: -1, y: 0 },
        ArrowRight: { x: 1, y: 0 },
        w: { x: 0, y: -1 },
        s: { x: 0, y: 1 },
        a: { x: -1, y: 0 },
        d: { x: 1, y: 0 },
        W: { x: 0, y: -1 },
        S: { x: 0, y: 1 },
        A: { x: -1, y: 0 },
        D: { x: 1, y: 0 }
      };

      function setDirection(dir) {
        if (!dir) return;
        // avoid reversing directly
        if (dir.x === -direction.x && dir.y === -direction.y) return;
        nextDirection = dir;
        if (isPaused && !isGameOver) {
          startLoop();
        }
      }

      document.addEventListener('keydown', (e) => {
        if (e.code === 'Space') {
          e.preventDefault();
          togglePause();
          return;
        }
        const dir = keyToDir[e.key];
        if (dir) {
          e.preventDefault();
          setDirection(dir);
        }
      });

      // Buttons
      btnToggle.addEventListener('click', () => {
        if (isGameOver) return; // do nothing if game over
        togglePause();
      });
      btnRestart.addEventListener('click', () => {
        resetGame();
        startLoop();
      });
      speedSlider.addEventListener('input', () => {
        baseTickMs = sliderToMs(parseInt(speedSlider.value, 10));
        updateSpeedLabel();
        if (!isPaused && !isGameOver) {
          stopLoop();
          startLoop();
        }
      });

      // D-Pad clicks
      dpad.addEventListener('click', (e) => {
        const key = e.target.closest('.dkey');
        if (!key) return;
        const dirName = key.getAttribute('data-dir');
        const map = { Up: {x:0,y:-1}, Down: {x:0,y:1}, Left: {x:-1,y:0}, Right: {x:1,y:0} };
        setDirection(map[dirName]);
      });

      // Swipe on canvas
      let touchStart = null;
      canvas.addEventListener('touchstart', (e) => {
        if (e.touches.length > 0) {
          touchStart = { x: e.touches[0].clientX, y: e.touches[0].clientY };
        }
      }, { passive: true });
      canvas.addEventListener('touchend', (e) => {
        if (!touchStart) return;
        const t = e.changedTouches[0];
        const dx = t.clientX - touchStart.x;
        const dy = t.clientY - touchStart.y;
        const absX = Math.abs(dx);
        const absY = Math.abs(dy);
        const threshold = 20; // px
        if (absX < threshold && absY < threshold) return; // ignore taps
        if (absX > absY) {
          setDirection({ x: dx > 0 ? 1 : -1, y: 0 });
        } else {
          setDirection({ x: 0, y: dy > 0 ? 1 : -1 });
        }
        touchStart = null;
      }, { passive: true });

      // Initialize
      resetGame();
      draw();
    </script>
  </body>
  </html>



